window.truck = null;

var LAMBORGHINI_URL =
  'http://sketchup.google.com/3dwarehouse/download?'
  + 'mid=450b8a68df03fb635d55c2fcd3ba45ed&rtyp=zip&'
  + 'fn=Untitled&ctyp=other&prevstart=12&ts=1221098006000';

var RACE_CAR_URL =
  'http://earthgames.appspot.com/Formula1.kmz';

var MOTORCYCLE_URL =
  'http://sketchup.google.com/3dwarehouse/download?'
  + 'mid=6e17e4501a177c5dabc0e2d1f2b1e3e&rtyp=zip&'
  + 'fn=Untitled&ctyp=other&prevstart=12&ts=1220089185000';

var HUMMER_URL =
  'http://sketchup.google.com/3dwarehouse/download?'
  + 'mid=c533e761975e6118ca8e0c77b23bb25&rtyp=zip&'
  + 'fn=hummer&ctyp=other&prevstart=12&ts=1218560716000';

var FUTURE_CAR_URL =
  'http://sketchup.google.com/3dwarehouse/download?'
  + 'mid=1059850d815640f07ae359c84a80bff0&rtyp=zip&'
  + 'fn=car&ctyp=other';

var MILKTRUCK_URL =
  'http://sketchup.google.com/3dwarehouse/download?'
  + 'mid=3c9a1cac8c73c61b6284d71745f1efa9&rtyp=zip&'
  + 'fn=milktruck&ctyp=milktruck';

Car.prototype.placemark = null;
Car.prototype.direction = 90;
Car.prototype.dX = 0;
Car.prototype.dY = 1;

var STEER_ROLL = -1.0;
var ROLL_SPRING = 0.95;
var ROLL_DAMP = -0.16;
var ACCEL = 60.0;
var DECEL = 80.0;
var GRAV_MULT = 5;
var TURN_SPEED_MIN = 60.0;  // radians/sec
var TURN_SPEED_MAX = 100.0;  // radians/sec
var SPEED_MAX_TURN = 25.0;
var SPEED_MIN_TURN = 120.0;
var MAX_REVERSE_SPEED = 40.0;
var DRAG_FACTOR = 0.0009;
var CONSTANT_DRAG = 2.0;

function Car(modelName) {

  var me = this;

  me.curLat = 0;
  me.curLon = 0;
  me.curHeading = 0;
  me.boost = 0;
  // We do all our motion relative to a local coordinate frame that is
  // anchored not too far from us.  In this frame, the x axis points
  // east, the y axis points north, and the z axis points straight up
  // towards the sky.
  //
  // We periodically change the anchor point of this frame and
  // recompute the local coordinates.
  me.localAnchorLla = [0, 0, 0];
  me.localAnchorCartesian = V3.latLonAltToCartesian(me.localAnchorLla);
  me.localFrame = M33.identity();

  // Position, in local cartesian coords.
  me.pos = [0, 0, 0];

  // Velocity, in local cartesian coords.
  me.vel = [0, 0, 0];

  // Orientation matrix, transforming model-relative coords into local
  // coords.
  me.modelFrame = M33.identity();

  me.roll = 0;
  me.rollSpeed = 0;

  me.accel = ACCEL;

  ge.getOptions().setFlyToSpeed(100);  // don't filter camera motion

  window.google.earth.fetchKml(ge, getModelUrl(modelName ? modelName : "default"),
                               function(obj) { me.finishInit(obj); });
}

function getModelUrl(name) {
  switch (name.toLowerCase()) {
  case "lamborghini": return LAMBORGHINI_URL;
  case "motorcyle":   return MOTORCYCLE_URL;
  case "hummer":      return HUMMER_URL;
  case "milktruck":   return MILKTRUCK_URL;
  case "speeder":     return FUTURE_CAR_URL;
  case "formula1":
  default:            return RACE_CAR_URL;
  }
}

// This method works with a latch to allow setting of properties
// before they can be used.  If either moveToPos or setHeading (which
// calls moveToPos) is called before a model (and so its location) is
// available, the params of those methods are saved as member vars.
// This method will call those methods again with the stored values or
// their defaults if they have not been set in this conditional.
Car.prototype.finishInit = function(kml) {
  var me = this;

  me.placemark = kml.getFeatures().getChildNodes().item(1);
  me.model = me.placemark.getGeometry();
  me.orientation = me.model.getOrientation();
  me.location = me.model.getLocation();

  me.model.setAltitudeMode(ge.ALTITUDE_ABSOLUTE);
  me.orientation.setHeading(180);

  ge.getFeatures().appendChild(me.placemark);

  // See latch in moveToPos.
  me.moveToPos(me.curLat, me.curLon, me.curHeading);

  me.lastMillis = (new Date()).getTime();

  var href = window.location.href;
  var pagePath = href.substring(0, href.lastIndexOf('/')) + '/';

  me.shadow = ge.createGroundOverlay('');
  me.shadow.setVisibility(false);
  me.shadow.setIcon(ge.createIcon(''));
  me.shadow.setLatLonBox(ge.createLatLonBox(''));
  me.shadow.setAltitudeMode(ge.ALTITUDE_CLAMP_TO_GROUND);
  me.shadow.getIcon().setHref(pagePath + 'shadowrect.png');
  me.shadow.setVisibility(true);
  ge.getFeatures().appendChild(me.shadow);

  // TODO(pmy) hack, move this somewhere better.
  placemarks[me.placemark.getName()] = me.placemark;
  placemarks[me.placemark.getName() + '-shadow'] = me.shadow;

  google.earth.addEventListener(ge, "frameend", function() { me.tick(); });

  me.cameraCut();
}

Car.prototype.isLoaded = function() {
  return this.model != null;
}

Car.prototype.getCoord = function() {
  var Coord = function(x, y) {
    this.x = x;
    this.y = y;
    coord.lat = function() {
      return this.x;
    }
    coord.lng = function() {
      return this.y;
    }
  };
  return new Coord(this.location.getLatitude(), this.location.getLongitude());
};

Car.prototype.getLatitude = function() {
  if (!this.location)
    return 0;
  return this.location.getLatitude();
}

Car.prototype.getLongitude = function() {
  if (!this.location)
    return 0;
  return this.location.getLongitude();
}

Car.prototype.getHeading = function() {
  if (!this.orientation)
    return 0;
  return this.orientation.getHeading();
}

Car.prototype.setHeading = function(heading) {
  this.moveToPos(this.curLat, this.curLon, heading);
}

Car.prototype.getAcceleration = function() {
  return this.accel;
}

Car.prototype.setAcceleration = function(acceleration) {
  this.accel = acceleration;
}

Car.prototype.setBoost = function(boost) {
  this.boost = boost;
}

Car.prototype.getBoost = function() {
  return this.boost;
}

/**
 * Degrade turning at higher speeds.
 *
 *           angular turn speed vs. vehicle speed
 *    |     -------
 *    |    /       \-------
 *    |   /                 \-------
 *    |--/                           \---------------
 *    |
 *    +-----+-------------------------+-------------- speed
 *    0    SPEED_MAX_TURN           SPEED_MIN_TURN
 */
Car.prototype.setTurnSpeed = function() {
  var ts, absSpeed = this.absSpeed;
  if (absSpeed < SPEED_MAX_TURN) {
    ts = TURN_SPEED_MIN + (TURN_SPEED_MAX - TURN_SPEED_MIN)
    * (SPEED_MAX_TURN - absSpeed) / SPEED_MAX_TURN;
    ts *= (absSpeed / SPEED_MAX_TURN);  // Less turn as truck slows
  } else if (absSpeed < SPEED_MIN_TURN) {
    ts = TURN_SPEED_MIN + (TURN_SPEED_MAX - TURN_SPEED_MIN)
    * (SPEED_MIN_TURN - absSpeed)
    / (SPEED_MIN_TURN - SPEED_MAX_TURN);
  } else {
    ts = TURN_SPEED_MIN;
  }
  this.turnSpeed = ts;
}

Car.prototype.turnLeft = function() {
  this.setTurnSpeed();
  this.steerAngle = this.turnSpeed * this.dt * Math.PI / 180.0;
}

Car.prototype.turnRight = function() {
  this.setTurnSpeed();
  this.steerAngle = -this.turnSpeed * this.dt * Math.PI / 180.0;
}

Car.prototype.accelerate = function(dir) {
  if (this.airborne)
    return;
  this.vel = V3.add(this.vel, V3.scale(this.dir, (this.boost * this.dt) + (this.accel * this.dt)));
}

Car.prototype.decelerate = function(dir) {
  if (this.airborne)
    return;
  if (this.forwardSpeed > -MAX_REVERSE_SPEED)
    this.vel = V3.add(this.vel, V3.scale(this.dir, -DECEL * this.dt));
}

Car.prototype.jump = function(amt) {
  if (this.airborne)
    return;
  this.vel = V3.add(this.vel, [0, 0, amt]);
}

Car.prototype.tick = function() {
  var me = this;

  var now = (new Date()).getTime();
  // dt is the delta-time since last tick, in seconds
  me.dt = (now - me.lastMillis) / 1000.0;
  if (me.dt > 0.25) {
    me.dt = 0.25;
  }
  me.boost *= 0.5;
  if (me.boost < 1)
    me.boost = 0;
  me.lastMillis = now;

  var c0 = 1;
  var c1 = 0;

  var gpos = V3.add(me.localAnchorCartesian,
                    M33.transform(me.localFrame, me.pos));
  var lla = V3.cartesianToLatLonAlt(gpos);

  if (V3.length([me.pos[0], me.pos[1], 0]) > 100) {
    // Re-anchor our local coordinate frame whenever we've strayed a
    // bit away from it.  This is necessary because the earth is not
    // flat!
    me.adjustAnchor();
  }

  me.dir = me.modelFrame[1];
  var up = me.modelFrame[2];

  var FLOAT_HEIGHT = 0;
  var AIRBORNE_HEIGHT = 1;
  var groundAlt = ge.getGlobe().getGroundAltitude(lla[0], lla[1]);
  me.airborne = (groundAlt + (FLOAT_HEIGHT + AIRBORNE_HEIGHT) < me.pos[2]);

  me.absSpeed = V3.length(me.vel);
  me.steerAngle = 0;

  me.forwardSpeed = 0;

  if (!me.airborne) {
    // TODO: if we're slipping, transfer some of the slip
    // velocity into forward velocity.

    // Damp sideways slip.  Ad-hoc frictiony hack.
    //
    // I'm using a damped exponential filter here, like:
    // val = val * c0 + val_new * (1 - c0)
    //
    // For a variable time step:
    //  c0 = exp(-me.dt / TIME_CONSTANT)
    var right = me.modelFrame[0];
    var slip = V3.dot(me.vel, right);
    c0 = Math.exp(-me.dt / 0.5);
    me.vel = V3.sub(me.vel, V3.scale(right, slip * (1 - c0)));

    // Apply engine/reverse accelerations.
    me.forwardSpeed = V3.dot(me.dir, me.vel);
  }

  if (typeof(everyFrame) != 'undefined')
    everyFrame();

  // Apply turn.
  var newdir = me.airborne ? me.dir : V3.rotate(me.dir, up, me.steerAngle);
  me.modelFrame = M33.makeOrthonormalFrame(newdir, up);
  me.dir = me.modelFrame[1];
  up = me.modelFrame[2];

  // Air drag.
  //
  // Fd = 1/2 * rho * v^2 * Cd * A.
  // rho ~= 1.2 (typical conditions)
  // Cd * A = 3 m^2 ("drag area")
  //
  // I'm simplifying to:
  //
  // accel due to drag = 1/Mass * Fd
  // with Milktruck mass ~= 2000 kg
  // so:
  // accel = 0.6 / 2000 * 3 * v^2
  // accel = 0.0009 * v^2
  me.absSpeed = V3.length(me.vel);
  if (me.absSpeed > 0.01) {
    var veldir = V3.normalize(me.vel);
    var drag = me.absSpeed * me.absSpeed * DRAG_FACTOR;

    // Some extra constant drag (rolling resistance etc) to make sure
    // we eventually come to a stop.
    drag += CONSTANT_DRAG;

    if (drag > me.absSpeed) {
      drag = me.absSpeed;
    }

    me.vel = V3.sub(me.vel, V3.scale(veldir, drag * me.dt));
  }

  // Gravity
  me.vel[2] -= GRAV_MULT * 9.8 * me.dt;

  // Move.
  var deltaPos = V3.scale(me.vel, me.dt);
  me.pos = V3.add(me.pos, deltaPos);

  gpos = V3.add(me.localAnchorCartesian,
                M33.transform(me.localFrame, me.pos));
  lla = V3.cartesianToLatLonAlt(gpos);

  // Don't go underground.
  groundAlt = ge.getGlobe().getGroundAltitude(lla[0], lla[1]) + FLOAT_HEIGHT;
  if (me.pos[2] < groundAlt) {
    me.pos[2] = groundAlt;
  }

  var normal = estimateGroundNormal(gpos, me.localFrame);

  if (!me.airborne) {
    // Cancel velocity into the ground.
    //
    // TODO: would be fun to add a springy suspension here so
    // the truck bobs & bounces a little.
    var speedOutOfGround = V3.dot(normal, me.vel);
    if (speedOutOfGround < 0) {
      me.vel = V3.add(me.vel, V3.scale(normal, -speedOutOfGround));
    }

    // Make our orientation follow the ground.
    c0 = Math.exp(-me.dt / 0.25);
    c1 = 1 - c0;
    var blendedUp = V3.normalize(V3.add(V3.scale(up, c0),
                                        V3.scale(normal, c1)));
    me.modelFrame = M33.makeOrthonormalFrame(me.dir, blendedUp);
  }

  // Propagate our state into Earth.
  gpos = V3.add(me.localAnchorCartesian,
                M33.transform(me.localFrame, me.pos));
  lla = V3.cartesianToLatLonAlt(gpos);
  me.model.getLocation().setLatLngAlt(lla[0], lla[1], lla[2]);

  var newhtr = M33.localOrientationMatrixToHeadingTiltRoll(me.modelFrame);

  // Compute roll according to steering.
  // TODO: this would be even more cool in 3d.
  var absRoll = newhtr[2];
  me.rollSpeed += me.steerAngle * me.forwardSpeed * STEER_ROLL;
  // Spring back to center, with damping.
  me.rollSpeed += (ROLL_SPRING * -me.roll + ROLL_DAMP * me.rollSpeed);
  me.roll += me.rollSpeed * me.dt;
  me.roll = clamp(me.roll, -30, 30);
  absRoll += me.roll;

  me.orientation.set(newhtr[0], newhtr[1], absRoll);

  var latLonBox = me.shadow.getLatLonBox();
  var radius = .00002;
  latLonBox.setNorth(lla[0] - radius);
  latLonBox.setSouth(lla[0] + radius);
  latLonBox.setEast(lla[1] - radius);
  latLonBox.setWest(lla[1] + radius);
  latLonBox.setRotation(-newhtr[0]);

  me.cameraFollow(me.dt, gpos, me.localFrame);

  // Hack to work around focus bug
  // TODO: fix that bug and remove this hack.
  ge.getWindow().blur();
};

// TODO: would be nice to have globe.getGroundNormal() in the API.
function estimateGroundNormal(pos, frame) {
  // Take four height samples around the given position, and use it to
  // estimate the ground normal at that position.
  //  (North)
  //     0
  //     *
  //  2* + *3
  //     *
  //     1
  var pos0 = V3.add(pos, frame[0]);
  var pos1 = V3.sub(pos, frame[0]);
  var pos2 = V3.add(pos, frame[1]);
  var pos3 = V3.sub(pos, frame[1]);
  var globe = ge.getGlobe();
  function getAlt(p) {
    var lla = V3.cartesianToLatLonAlt(p);
    return globe.getGroundAltitude(lla[0], lla[1]);
  }
  var dx = getAlt(pos1) - getAlt(pos0);
  var dy = getAlt(pos3) - getAlt(pos2);
  var normal = V3.normalize([dx, dy, 2]);
  return normal;
}

// Cut the camera to look at me.
Car.prototype.cameraCut = function() {
  var me = this;
  var lo = me.model.getLocation();
  var la = ge.createLookAt('');
  la.set(lo.getLatitude(), lo.getLongitude(),
         0 /* altitude */,
         ge.ALTITUDE_RELATIVE_TO_GROUND,
         fixAngle(180 + me.model.getOrientation().getHeading() + 0),
         0, /* tilt */
         0 /* range */
         );
  ge.getView().setAbstractView(la);
};

Car.prototype.cameraFollow = function(dt, truckPos, localToGlobalFrame) {
  var me = this;

  var c0 = Math.exp(-dt / 0.5);
  var c1 = 1 - c0;

  var la = ge.getView().copyAsLookAt(ge.ALTITUDE_RELATIVE_TO_GROUND);

  var truckHeading = me.model.getOrientation().getHeading();
  var camHeading = la.getHeading();

  var deltaHeading = fixAngle(truckHeading - camHeading);
  var heading = camHeading + c1 * deltaHeading;
  heading = fixAngle(heading);

  var TRAILING_DISTANCE = 40;
  var headingRadians = heading / 180 * Math.PI;

  var CAM_HEIGHT = 10;

  var headingDir = V3.rotate(localToGlobalFrame[1], localToGlobalFrame[2],
                             -headingRadians);
  var camPos = V3.add(truckPos, V3.scale(localToGlobalFrame[2], CAM_HEIGHT));
  camPos = V3.add(camPos, V3.scale(headingDir, -TRAILING_DISTANCE));
  var camLla = V3.cartesianToLatLonAlt(camPos);
  var camLat = camLla[0];
  var camLon = camLla[1];
  var camAlt = camLla[2] - ge.getGlobe().getGroundAltitude(camLat, camLon);

  la.set(camLat, camLon, camAlt, ge.ALTITUDE_RELATIVE_TO_GROUND,
        heading, 80 /*tilt*/, 0 /*range*/);
  ge.getView().setAbstractView(la);
};

Car.prototype.moveTo = function(loc) {
  var me = this;
  geoLookup(loc, function(point) { if (point == null) return;
                                   me.moveToPos(point.y, point.x, 0); });
}

Car.prototype.moveToCourseStart = function(course) {
  var s = course.getStartCoord();
  if (s == null) return;
  var h = course.getStartHeading();
  log(s.lat() + 'N' + s.lng() + 'E @ ' + h);
  this.moveToPos(s.lat(), s.lng(), h);
}

Car.prototype.moveToPos = function(lat, lon, heading) {
  var me = this;

  me.curLat = lat;
  me.curLon = lon;
  if (heading == null)
    heading = 0;
  me.curHeading = heading;

  // LATCH. See finishInit.
  if (!me.model)
    return;

  me.model.getLocation().setLatitude(lat);
  me.model.getLocation().setLongitude(lon);
  me.model.getLocation().setAltitude(ge.getGlobe().getGroundAltitude(lat, lon));

  me.vel = [0, 0, 0];

  me.localAnchorLla = [lat, lon, 0];
  me.localAnchorCartesian = V3.latLonAltToCartesian(me.localAnchorLla);
  me.localFrame = M33.makeLocalToGlobalFrame(me.localAnchorLla);
  me.modelFrame = M33.identity();
  me.modelFrame[0] = V3.rotate(me.modelFrame[0], me.modelFrame[2], -heading);
  me.modelFrame[1] = V3.rotate(me.modelFrame[1], me.modelFrame[2], -heading);
  me.pos = [0, 0, ge.getGlobe().getGroundAltitude(lat, lon)];

  me.cameraCut();
};

// Move our anchor closer to our current position.  Retain our global
// motion state (position, orientation, velocity).
Car.prototype.adjustAnchor = function() {
  var me = this;
  var oldLocalFrame = me.localFrame;

  var globalPos = V3.add(me.localAnchorCartesian,
                         M33.transform(oldLocalFrame, me.pos));
  var newAnchorLla = V3.cartesianToLatLonAlt(globalPos);
  newAnchorLla[2] = 0;  // For convenience, anchor always has 0 altitude.

  var newAnchorCartesian = V3.latLonAltToCartesian(newAnchorLla);
  var newLocalFrame = M33.makeLocalToGlobalFrame(newAnchorLla);

  var oldFrameToNewFrame = M33.transpose(newLocalFrame);
  oldFrameToNewFrame = M33.multiply(oldFrameToNewFrame, oldLocalFrame);

  var newVelocity = M33.transform(oldFrameToNewFrame, me.vel);
  var newModelFrame = M33.multiply(oldFrameToNewFrame, me.modelFrame);
  var newPosition = M33.transformByTranspose(
      newLocalFrame,
      V3.sub(globalPos, newAnchorCartesian));

  me.localAnchorLla = newAnchorLla;
  me.localAnchorCartesian = newAnchorCartesian;
  me.localFrame = newLocalFrame;
  me.modelFrame = newModelFrame;
  me.pos = newPosition;
  me.vel = newVelocity;
}
